<?

// required data types
define( 'STR_REQUIRED', 1 );
define( 'INT_REQUIRED', 2 );
// optional data types
define( 'INT_OPTIONAL', 4 );
define( 'STR_OPTIONAL', 5 );

class Smutty_Model {

	// private objects
	var $_data, $_hasOneCache, $_hasManyCache;

	// the name of this models table, defaults to false so it will be
	// worked out dynamically, but you can set it to the actual table
	// name if needed.
	var $tableName = false;

	// allows specifying dependent models by their name.  the related
	// field is assumed to be {model}_id, but you can specify it if
	// you need to by using dot notation like so:
	//   var $has_one = 'User.myuservar';
	//  multiple entries should be space seperated.
	var $has_many = '';
	var $has_one = '';

	// this property should be used to specify how data is to
	// be validated.  it should be a hash of the name of the
	// property and then a constant indicating it's type and
	// it's "requiredness"
	var $validate = array();

	/**
	 *  constructor
	 *
	 */

	function Smutty_Model() {
		$this->_data = new stdclass();
		$this->_hasOneCache = array();
		$this->_hasManyCache = array();
		$this->errors = array();
	}

	/**
	 *  this method tries to automatically fill a
	 *  model with data that has been passed to
	 *  the application.
	 *
	 */

	function fill() {
		$data = Smutty_Data::getInstance();
		$fields = Smutty_Model::getFields( get_class($this) );
		foreach ( $fields as $field ) {
			$name = $field->name;
			$this->$name = $this->getFieldValue(
				$field, $data->string($name)
			);;
		}
	}

	/**
	 *  populates the model with data from the array
	 *
	 *  @param [data] a hash of name/value pairs
	 *
	 */

	function _populate( $data ) {
		foreach ( $data as $key => $value )
			$this->$key = $value;
	}

	/**
	 *  returns the name of the table this model is
	 *  associated with.
	 *
	 *  @param String table name
	 *
	 */

	function getTable( $class ) {
		$model = new $class();
		return $model->tableName
			? $model->tableName
			: strtolower( Smutty_Utils::pluralize($class) );
	}

	/**
	 *  returns an array of this models fields and their types
	 *
	 *  @return array of fields
	 *
	 */

	function getFields( $class ) {
		$db = Smutty_Database::getInstance();
		$cfg = Smutty_Config::getInstance();
		$tblName = Smutty_Model::getTable( $class );
		$sql = " desc `$tblName` ";
		$res = $db->query( $sql );
		$array = array();
		while ( $row = $res->fetch() ) {
			$field = new stdclass();
			$field->name = $row[ 0 ];
			// strip field sizes, just want the basic type
			$field->type = preg_replace( '/^(.*)\(.*$/', '$1', $row[1] );
			$array[] = $field;
		}
		return $array;
	}

	/**
	 *  this function allows searching for a record by
	 *  it's unique id.  if it is found then an instance
	 *  of it's model object will be returned, otherwise
	 *  you'll get false.
	 *
	 *  @param [id] the id to search for
	 *  @param Model the records model
	 *
	 */

	function find( $class, $id ) {
		$db = Smutty_Database::getInstance();
		$tblName = Smutty_Model::getTable( $class );
		$sql = " select *
				from `$tblName`
				where id = '$id' ";
		$res = $db->query( $sql );
		if ( $row = $res->fetchAssoc() ) {
			$model = new $class();
			$model->_populate( $row );
			return $model;
		}
		else return false;
	}

	/**
	 *  returns all the records for this model
	 *
	 *  @return array Model objects
	 *
	 */

	function fetchAll( $class, $order = false, $params = array(), $limit = false ) {

		// specify an order by?
		$orderSql = '';
		if ( $order ) {
			$parts = explode( '.', $order );
			$orderSql = " order by `$parts[0]` " .
				( $parts[1] ? " $parts[1] " : '' );
		}

		// limit results?
		$limitSql = '';
		if ( $limit = Smutty_Data::getInt($limit) )
			$limitSql = " limit $limit ";

		// other params
		$whereSql = '';
		if ( is_array($params) )
			foreach ( $params as $key => $value )
				$whereSql = " and ( `$key` = '$value' ) ";
		$whereSql = substr( $whereSql, 4 );
		if ( $whereSql ) $whereSql = " where $whereSql ";

		// generate sql for query
		$db = Smutty_Database::getInstance();
		$tblName = Smutty_Model::getTable( $class );
		$sql = " select *
				from `$tblName`
				$whereSql
				$orderSql
				$limitSql ";
		if ( !$res = $db->query($sql) )
			new Smutty_Exception( $db->getError() );

		// put results in array ready to return
		$array = array();
		while ( $row = $res->fetchAssoc() ) {
			$model = new $class();
			$model->_populate( $row );
			$array[] = $model;
		}

		return $array;

	}

	/**
	 *  handles storing of model properties
	 *
	 *  @param [name] the name of the property
	 *  @param [value] the properties value
	 *
	 */

	function __set( $name, $value ) {
		$this->_data->$name = $value;
		return true;
	}

	/**
	 *  this function checks if the models data is currently in a
	 *  valid state to be saved.  this means that all validation
	 *  criteria that have been specified need to be met.
	 *
	 *  errors are stored in $this->errors and can be accessed
	 *  by using the $this->getErrors() function.
	 *
	 *  @return boolean
	 *
	 */

	function isValid() {

		$errors = array();
		$fields = Smutty_Model::getFields( get_class($this) );

		foreach ( $fields as $field ) {

			$name = $field->name;
			$value = $this->$name;

			if ( $criteria = $this->validate[$name] )
				switch ( $criteria ) {
					case STR_REQUIRED:
						if ( !$value )
							array_push( $errors, " $name " . ERR_VALIDATE_REQUIRED );
						break;
					case INT_REQUIRED:
						$value = Smutty_Data::getInt( $value );
						if ( !$value )
							array_push( $errors, " $name " . ERR_VALIDATE_REQUIRED );
						break;
				}

		}

		$this->errors = $errors;
		return ( !$this->errors );

	}

	/**
	 *  returns an array containing any errors that were encountered the
	 *  last time the model was validated.
	 *
	 *  @return array
	 *
	 */

	function getErrors() {

		return $this->errors;

	}

	/**
	 *  handles access to model variables, needs to check the
	 *  relations (has_one,has_many) that have been defined
	 *  to give automagic access.
	 *
	 *  @param [name] the proprty name
	 *  @param [value] reference to the return variable
	 *
	 */

	function __get( $name, &$value ) {

		// check for "has one" relations
		if ( $this->has_one ) {
			$parts = explode( ' ', $this->has_one );
			foreach ( $parts as $part ) {
				$names = explode( '.', $part );
				$modelName = $names[ 0 ];
				$fieldName = $names[1] ? $names[1] : strtolower($modelName) . '_id';
				$propName = $names[2] ? $names[2] : strtolower($modelName);
				// have we found a related field we need to load?
				if ( $name == $propName ) {
					$id = $this->_data->$fieldName;
					$cacheID = $propName . $id;
					// check cache for model, if not add to cache
					if ( !$this->_hasOneCache[$cacheID] ) {
						eval( "\$model = $modelName::find(\$id);" );
						$this->_hasOneCache[$cacheID] = $model;
					}
					$value = $this->_hasOneCache[$cacheID];
					return true;
				}
			}
		}

		// check for "has many" relations
		if ( $this->has_many ) {
			$parts = explode( ' ', $this->has_many );
			foreach ( $parts as $part ) {
				$names = explode( '.', $part );
				$modelName = $names[ 0 ];
				$fieldName = $names[1] ? $names[1] : strtolower(get_class($this)) . '_id';
				$plural = Smutty_Utils::pluralize(strtolower($modelName));
				if ( $name == $plural ) {
					$id = $this->_data->id;
					$cacheID = $modelName . $fieldName . $id;
					if ( !$this->_hasManyCache[$cacheID] ) {
						eval( "\$models = $modelName::fetchAll(false,array($fieldName => $id));" );
						$this->_hasManyCache[$cacheID] = $models;
					}
					$value = $this->_hasManyCache[$cacheID];
					return true;
				}
			}
		}

		// assume normal field
		$value = $this->_data->$name;
		return true;

	}

	/**
	 *  determines if a record with the specified id
	 *  currently exists in this models table
	 *
	 *  @param [id] the id to search for
	 *  @return boolean
	 *
	 */

	function exists( $id ) {
		$db = Smutty_Database::getInstance();
		$table = Smutty_Model::getTable( get_class($this) );
		$id = $db->quote( $id );
		$sql = " select 1
				from `$table`
				where id = '$id' ";
		$res = $db->query( $sql );
		return ( $res->fetch() );
	}

	/**
	 *  returns the sql for UPDATING this models record
	 *
	 *  @return String
	 *
	 */

	function getUpdateSql() {

		$class = get_class( $this );
		$db = Smutty_Database::getInstance();
		$table = Smutty_Model::getTable( $class );
		$sql = '';
		$fields = Smutty_Model::getFields( $class );

		foreach ( $fields as $field ) {
			$name = $field->name;
			if ( $name == 'id' )
				continue;
			$value = $this->$name;
			$value = $db->quote( $value );
			$sql .= ", `$name` = '$value' ";
		}

		$sql = substr( $sql, 1 );
		$id = $db->quote( $this->id );
		$sql = " update `$table`
			 	set $sql
			 	where id = '$id' ";

		return $sql;

	}

	/**
	 *  returns a value properly formatted for the specified field type
	 *
	 *  @param [field] a field (name/value) object
	 *  @param [value] the desired field value
	 *  @return String
	 *
	 */

	function getFieldValue( $field, $value ) {
		$db = Smutty_Database::getInstance();
		switch ( $field->type ) {
			case 'int':
				return (int) $value;
				break;
			case 'datetime':
				// fall through...
			default:
				return $db->quote( $value );
		}
	}

	/**
	 *  returns the sql for inserting a new record with this
	 *  models data.
	 *
	 *  @return String
	 *
	 */

	function getInsertSql() {

		$class = get_class( $this );
		$db = Smutty_Database::getInstance();
		$sqlFields = '';
		$sqlValues = '';
		$fields = Smutty_Model::getFields( $class );
		$table = Smutty_Model::getTable( $class );

		foreach ( $fields as $field ) {
			$name = $field->name;
			if ( $name == 'id' )
				continue;
			$value = $this->$name;
			$value = $this->getFieldValue( $field, $value );
			$sqlFields .= ", $field->name";
			$sqlValues .= ", '$value' ";
		}

		$sqlFields = substr( $sqlFields, 1 );
		$sqlValues = substr( $sqlValues, 1 );
		$sql = " insert into `$table`
			 	( $sqlFields )
			 	values ( $sqlValues ); ";

		return $sql;

	}

	/**
	 *  this function saves a models data
	 *
	 */

	function save() {

		$db = Smutty_Database::getInstance();
		$sql = null;

		// first we need to do any data validation that is required
		if ( !$this->isValid() )
			return false;

		// existing record or new one?
		if ( $this->id && $this->exists($this->id) )
			$sql = $this->getUpdateSql();
		else
			$sql = $this->getInsertSql();

		if ( $db->update($sql) ) {
			if ( !$this->id )
				$this->id = $db->getInsertId();
			return true;
		}
		else return false;

	}

}

?>